<html>
  <head>
    <meta charset="utf-8" />
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>来学一波 Navigation | Jiang</title>
<link rel="shortcut icon" href="https://cherrylover.github.io/favicon.ico?v=1601447716515">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<link rel="stylesheet" href="https://cherrylover.github.io/styles/main.css">

<script src="https://cdn.bootcss.com/highlight.js/9.12.0/highlight.min.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.23.0/moment.min.js"></script>


<script async src="https://www.googletagmanager.com/gtag/js?id=UA-143284233-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-143284233-1');
</script>

  </head>
  <body>
    <div class="main">
      <div class="main-content">
        <div class="site-header">
  <a href="https://cherrylover.github.io">
  <img class="avatar" src="https://cherrylover.github.io/images/avatar.png?v=1601447716515" alt="">
  </a>
  <h1 class="site-title">
    Jiang
  </h1>
  <p class="site-description">
    我允许你走进我的世界，但不许你在我的世界里走来走去。
  </p>
  <div class="menu-container">
    
      
        <a href="/" class="menu">
          首页
        </a>
      
    
      
        <a href="/archives" class="menu">
          归档
        </a>
      
    
      
        <a href="/tags" class="menu">
          标签
        </a>
      
    
      
        <a href="https://cherrylover.github.io/post/guan-yu-wo/" class="menu">
          关于
        </a>
      
    
  </div>
  <div class="social-container">
    
      
        <a href="https://github.com/CherryLover" target="_blank">
          <i class="fab fa-github"></i>
        </a>
      
    
      
    
      
        <a href="https://juejin.im/user/576d73cd0a2b58006a09ad87" target="_blank">
          <i class="fab fa-weibo"></i>
        </a>
      
    
      
    
      
    
  </div>
</div>

      
        <div class="post-detail">
          <article class="post">
            <h2 class="post-title">
              来学一波 Navigation
            </h2>
            <div class="post-info">
              <time class="post-time">
                · 2019-06-29 ·
              </time>
              
                <a href="https://cherrylover.github.io/tag/J97-m2V0f/" class="post-tags">
                  # Jetpack 追新
                </a>
              
                <a href="https://cherrylover.github.io/tag/37yorOBW4/" class="post-tags">
                  # 持续学习
                </a>
              
                <a href="https://cherrylover.github.io/tag/Mk4HmBIQ7/" class="post-tags">
                  # Android
                </a>
              
            </div>
            
              <div class="post-feature-image" style="background-image: url('https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog_navigation.jpg')">
              </div>
            
            <div class="post-content">
              <p>Navigation 是一个谷歌官方推出的一个用于 APP 内部便捷切换内容（Fragment 或 Activity）的库。从而使得 APP 内的页面跳转更简单。</p>
<p>我知道它的时候它的版本已经是 2.0.0 了，也是时候来学习一波了。</p>
<!-- more --> 
<p>无论什么时候，学习的第一手资料不能缺了官方出品的 <a href="https://codelabs.developers.google.com/codelabs/android-navigation/#0">CodeLab</a>。相信你，看了 CodeLab 之后就能对 Navigation 有一个简单的了解。本人也是对 CodeLab 学习之后才写下了这篇博客，主要内容都能在 CodeLab 上找到。不过 CodeLab 里面是英文的讲解，而且其中的代码是使用 Kotlin 编写的，这篇博客是以 Java 代码的方式进行的。</p>
<p>还一件事情，Navigation 的原生支持是从 Android Studio 3.3 开始的，3.2 版本的需要在设置面板的 Experimental 模块中启用 Navigation 编辑器。</p>
<figure data-type="image" tabindex="1"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628144412075.png" alt="image-20190628144412075" loading="lazy"></figure>
<blockquote>
<p>图片来自 CodeLab。</p>
</blockquote>
<p>下面开始正题</p>
<h2 id="navigation-graph-和-navhostfragment">Navigation Graph 和 NavHostFragment</h2>
<p>首先，添加依赖。</p>
<pre><code>implementation 'androidx.navigation:navigation-fragment:2.0.0'
implementation 'androidx.navigation:navigation-ui:2.0.0'
</code></pre>
<p>之后，在 res 文件夹下创建类型为 navigation 的资源文件夹，Android Studio 会自动在这个文件夹下生产一个名为 navigation.xml 的文件，这个文件的作用就是用于描述 Fragment 及相应的跳转逻辑、动画、参数等信息。这个文件也叫做 <strong>Navigation Graph</strong>。</p>
<figure data-type="image" tabindex="2"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628144907092.png" alt="create_navigation_folder" loading="lazy"></figure>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;navigation xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
            android:id=&quot;@+id/navigation&quot;&gt;

&lt;/navigation&gt;
</code></pre>
<p>默认的 Navigation Graph 文件就只有一个根节点，如果我们有更多的 Fragment，添加进来，会有不同的子节点，子节点代表的就是 Fragment，fragment 节点中描述关于 Fragment 的相关信息，并且在 fragment 节点中还可以其他子节点，比如，action、argument、deepLink。他们分别用于表示 Fragment 的相关信息。往后会讲到的。现在我们现在创建一个 Fragment ，就叫 RootFragment 好了。</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;FrameLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
             xmlns:tools=&quot;http://schemas.android.com/tools&quot;
             android:layout_width=&quot;match_parent&quot;
             android:layout_height=&quot;match_parent&quot;
             tools:context=&quot;.fragment.RootFragment&quot;&gt;
    &lt;TextView
            android:layout_gravity=&quot;center&quot;
            android:gravity=&quot;center&quot;
            android:layout_width=&quot;match_parent&quot;
            android:textSize=&quot;24sp&quot;
            android:layout_height=&quot;match_parent&quot;
            android:text=&quot;Root Fragment&quot;/&gt;

&lt;/FrameLayout&gt;
</code></pre>
<p>只是在页面上显示出这个 Fragment 的名字，Java 代码中没有做任何事情。现在让我们回到 Navigation Graph 中，我们是初学者，不知道或者说不了解 Fragment 节点有哪些属性可以去使用，可以使用 Navigation Graph 的图形化界面，刚才我们看了 Navigation Graph 的代码，现在来看一下，图形化编辑页面。</p>
<figure data-type="image" tabindex="3"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628150519705.png" alt="graph_design" loading="lazy"></figure>
<blockquote>
<p>左边区域：是已经添加进来的 Fragment 以及承载这些 Fragment 的页面；</p>
<p>中间区域：Fragment 的跳转示意图；</p>
<p>右边区域：是当前选中的 Fragment 的属性展示区；</p>
</blockquote>
<p>页面中间已经提示我们了，点击那个图标，添加一个目标。试试看吧，从 Android Studio 展示出的列表中，找到我们刚才创建的 RootFragment。这时，页面已经发生了变化。我们刚才创建的 RootFragment 的样子已经出现了，而且名称前还有一个小图标，这表示 RootFragment 是 Navigation 管理页面的第一个页面也是开始页面。</p>
<figure data-type="image" tabindex="4"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628150937246.png" alt="image-20190628150937246" loading="lazy"></figure>
<p>页面右侧出现了一些属性，我们暂时可以不用管，现在我只想先运行起来，看看效果。不过在这之前，我们还需要改造一下，之前新建项目自动生成的 MainActivity。先打开 activity_main.xml 的图形化编辑页面，然后在 Palette 类型列表中找到 <strong>NavHostFragment</strong> 并拖拽到页面上，此时会弹出一个框，让你选择 Navigation Graph，我们选择刚才自动创建的文件即可。</p>
<blockquote>
<p>Android Studio 的布局文件的拖拽，不是太好用，需要手动切换到源代码形式，简单改动一下页面代码，我们让这个 NavHostFrgament 组件填充满整个容器即可。</p>
</blockquote>
<figure data-type="image" tabindex="5"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628151745388.png" alt="image-20190628151745388" loading="lazy"></figure>
<p>最终的 activity_main.xml 的源文件如下：</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&gt;

    &lt;fragment
        android:id=&quot;@+id/fragment&quot;
        android:name=&quot;androidx.navigation.fragment.NavHostFragment&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        app:defaultNavHost=&quot;true&quot;
        app:navGraph=&quot;@navigation/navigation&quot; /&gt;
&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;
</code></pre>
<p>然后运行项目即可。这就是一个最简单的使用 Navigation 的例子，而且其中根本就没什么难度。</p>
<figure data-type="image" tabindex="6"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628161622969.png" alt="image-20190628161622969" loading="lazy"></figure>
<p>好，现在我们来回过头来看看，刚刚我们都做什么。我们真正有效的内容是从把 RootFragment 添加到 Navigation Graph 中，我们去看一下，Navigation Graph 的源代码。说不定能从那里发现点什么东西。</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;navigation xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:id=&quot;@+id/navigation&quot;
    app:startDestination=&quot;@id/rootFragment&quot;&gt;

    &lt;fragment
        android:id=&quot;@+id/rootFragment&quot;
        android:name=&quot;me.monster.blogtest.fragment.RootFragment&quot;
        android:label=&quot;fragment_root&quot;
        tools:layout=&quot;@layout/fragment_root&quot; /&gt;
&lt;/navigation&gt;
</code></pre>
<p>这个文件跟之前自动生成的没什么区别，无非就是多了一个 fragment 节点，以及根节点上多了一个 startDestination 属性。难道就是因为这个属性？是的，没错，在 Navigation 中我们使用 Destination(目标)来描述 Fragment 之间的跳转关系。这里的 startDestination 代表的就是这个是 Navigation 整个页面跳转管理栈的最根级页面。</p>
<p>再来看看那个添加到 MainActivity 页面的 NavHostFragment 组件。它其实就是一个布局文件中的 fragmen 组件，跟我们正常使用的没什么不同，非要说不同，那就是其中的 name、defaultNavHost 以及 navGraph 这三个属性了。</p>
<p>name 属性我们都知道，navGraph 属性里面的值是刚才创建 Navigation Graph，猜一下，就是把 Navigation Graph 引用到了这个 NavHostFragment 中。那最后一个 defaultNavHost 属性呢？那就是拦截系统返回按钮的点击事件的。</p>
<pre><code class="language-xml">&lt;fragment
    android:id=&quot;@+id/fragment&quot;
    android:name=&quot;androidx.navigation.fragment.NavHostFragment&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    app:defaultNavHost=&quot;true&quot;
    app:navGraph=&quot;@navigation/navigation&quot; /&gt;
</code></pre>
<h2 id="navcontroller">NavController</h2>
<p>单单一个 Fragment 没啥意思，不好玩，这回我们再加一个页面(SettingsFragment)。尝试着从 RootFragment 页面点击按钮切换到 SettingsFragment 页面。然后在 SettingsFragment 页面点击按钮返回到 RootFragment 页面。</p>
<blockquote>
<p>说是 SettingsFragment，里面就一个 Button 一个 TextView，布局代码就不贴了。</p>
</blockquote>
<p>Fragment 准备好了，该往 Navigation Graph 里添加了，按照刚才添加 RootFragment 的方式再来一次，不过，这次比上次多一步。选中 RootFragment，点击 RootFragment 右边的小圆点然后牵引到右侧的 SettingsFragment。这样他们两个就建立一种关系。</p>
<figure data-type="image" tabindex="7"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628160038102.png" alt="image-20190628160038102" loading="lazy"></figure>
<p>来看一下源代码吧。我们发现，除了增加了一个 fragment 节点之外，原来的 RootFragment 的节点上还增加了一个子节点 action 。事实上，action 节点就是用来描述 Fragmen 之间的页面跳转的关系的，其中 destination 属性的值就是目标 fragment 的 id。</p>
<pre><code class="language-xml">&lt;fragment
    android:id=&quot;@+id/rootFragment&quot;
    android:name=&quot;me.monster.blogtest.fragment.RootFragment&quot;
    android:label=&quot;fragment_root&quot;
    tools:layout=&quot;@layout/fragment_root&quot; /&gt;

&lt;!--上面是原来的代码，下面是新代码--&gt;

&lt;fragment
    android:id=&quot;@+id/rootFragment&quot;
    android:name=&quot;me.monster.blogtest.fragment.RootFragment&quot;
    android:label=&quot;fragment_root&quot;
    tools:layout=&quot;@layout/fragment_root&quot; &gt;
    &lt;action
        android:id=&quot;@+id/action_rootFragment_to_settingsFragment2&quot;
        app:destination=&quot;@id/settingsFragment&quot; /&gt;
&lt;/fragment&gt;
&lt;fragment
    android:id=&quot;@+id/settingsFragment&quot;
    android:name=&quot;me.monster.blogtest.fragment.SettingsFragment&quot;
    android:label=&quot;SettingsFragment&quot; /&gt;
</code></pre>
<p>继续往下，我们为 RootFragment 页面绑定点击事件。</p>
<pre><code class="language-java">private void toSettings() {
    btnToSettings.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Navigation.findNavController(btnToSettings)
                    .navigate(R.id.action_rootFragment_to_settingsFragment);
        }
    });
}
</code></pre>
<p>这一看就知道了，通过 Navigation 找到一个叫 <strong>NavController</strong> 的东西，然后执行 navigate 方法，这个方法里面传的值就是刚才 RootFragment 子节点 action 的 id 的值。先运行一下看看效果。</p>
<blockquote>
<ol>
<li>亲测点击按钮能跳转到 SettingsFragment 页面。下面的 Gif 动图只是表示能从 RootFragment 到 SettingsFragment，闪回到 RootFragment 页面只是 Gif 的重新播放。</li>
<li>如果你在 SettingsFragment 点击系统的返回键，是能返回到 RootFragment。这就是 MainActivity 中 NavHostFragment 组件的属性 <code>app:defaultNavHost=&quot;true&quot;</code> 起到的作用，有兴趣的话，可以改成 false 然后再试一下效果。</li>
</ol>
</blockquote>
<figure data-type="image" tabindex="8"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/Blognav_toSettings.gif" alt="" loading="lazy"></figure>
<p>现在，让我们再次为 SettingsFragment 添加按钮的点击事件吧。</p>
<pre><code class="language-java">private void goBack() {
    btnToRoot.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Navigation.findNavController(btnToRoot)
                    .popBackStack();
        }
    });
}
</code></pre>
<p>和之前跳转到这个页面的方式差不多，只是最后执行的方法变成了 <code>popBackStack</code> 。</p>
<p>嗯，挺好的，不过，我们有些时候需要在两个 Fragment 之间做切换动画，这个怎么办？这个也不难，在Navigation Graph 中跳转的 action 内增加属性即可。呐，这样就行了，而且还可以用过 Java 代码来实现。</p>
<pre><code class="language-xml">&lt;fragment
    android:id=&quot;@+id/rootFragment&quot;
    android:name=&quot;me.monster.blogtest.fragment.RootFragment&quot;
    android:label=&quot;fragment_root&quot;
    tools:layout=&quot;@layout/fragment_root&quot;&gt;
    &lt;action
        android:id=&quot;@+id/action_rootFragment_to_settingsFragment&quot;
        app:destination=&quot;@id/settingsFragment&quot;
        app:enterAnim=&quot;@anim/slide_in_right&quot;
        app:exitAnim=&quot;@anim/slide_out_left&quot;
        app:popEnterAnim=&quot;@anim/slide_in_left&quot;
        app:popExitAnim=&quot;@anim/slide_out_right&quot; /&gt;
&lt;/fragment&gt;
</code></pre>
<p>Java 代码</p>
<pre><code class="language-java">Navigation.findNavController(btnToSettings)
    .navigate(R.id.action_rootFragment_to_settingsFragment);
//                上面是原来的代码，下面是新代码

NavOptions options = new NavOptions.Builder()
    .setEnterAnim(R.anim.slide_in_right)
    .setExitAnim(R.anim.slide_out_left)
    .setPopEnterAnim(R.anim.slide_in_left)
    .setPopExitAnim(R.anim.slide_out_right)
    .build();

Navigation.findNavController(btnToSettings)
    .navigate(R.id.action_rootFragment_to_settingsFragment, null, options);
</code></pre>
<p>这里，我们调用了 navigate 这个方法，其中第二个参数是 Bundle 类型，我们填入了 null</p>
<p>，那如果正常填了值，Bundle 是不是就是传递到 SettingsFragment 了呢？答案是肯定的。不过 Navigation 还有另一种方式来传值—— <strong>Safe Args</strong>。</p>
<h3 id="safe-args">Safe Args</h3>
<blockquote>
<p>为啥要用 Safe Args 呢？</p>
<p>我也不知道为啥学，感觉如果单纯为了保证 key 安全的话，把 Bundle 里面的 key 抽取成常量值不也行吗？不太懂为啥要通过这种形式来做，不过呢，老话说得好，技多不压身。</p>
</blockquote>
<p>Safe Args 是配合 Navigation 使用的一个 Gradle 插件。首先你得先去配置：</p>
<p>首先在你项目的根目录的 build.gradle 文件中加上这些东西：</p>
<pre><code>repositories {
    google()
}
dependencies {
    classpath &quot;androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0&quot;
}
</code></pre>
<p>然后还得在你的 app 或是 module 的目录下的 build.gradle 文件夹加入：</p>
<pre><code>apply plugin: &quot;androidx.navigation.safeargs&quot;
</code></pre>
<p>如果你想用 safe Args 生成的代码时 Kotlin 的话，还需要加入：</p>
<pre><code>aapply plugin: &quot;androidx.navigation.safeargs.kotlin&quot;
</code></pre>
<p>最最最重要的一点是，你要确认你的 build.properties 文件中有这么一行：</p>
<pre><code class="language-java">android.useAndroidX=true
</code></pre>
<p>当然了，如果你的项目本身就是用是 AndroidX 的依赖，就不用去确认了，肯定能通过的嘛。</p>
<p>现在我们就来从 RootFragmet 传递一个类型为 String 的备注名到 SettingsFragmen 吧。还是先通过图形化界面进行设置吧，选中 SettingsFragment，然后再右侧属性面板上找到 Argments 点击旁边的➕。</p>
<p>弹出一个框，我们填入一下信息，然后点击 add。</p>
<figure data-type="image" tabindex="9"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628191327486.png" alt="image-20190628191327486" loading="lazy"></figure>
<p>完成之后的 Navigation Graph 中 SettingsFragment 节点的内容变了。</p>
<pre><code class="language-xml">&lt;fragment
    android:id=&quot;@+id/settingsFragment&quot;
    android:name=&quot;me.monster.blogtest.fragment.SettingsFragment&quot;
    android:label=&quot;SettingsFragment&quot;/&gt;

&lt;!--上面是原来的代码，下面是新代码--&gt;

&lt;fragment
    android:id=&quot;@+id/settingsFragment&quot;
    android:name=&quot;me.monster.blogtest.fragment.SettingsFragment&quot;
    android:label=&quot;SettingsFragment&quot;&gt;
    &lt;argument
        android:name=&quot;nickName&quot;
        android:defaultValue=&quot;未设置&quot;
        app:argType=&quot;string&quot;
        app:nullable=&quot;true&quot; /&gt;
&lt;/fragment&gt;
</code></pre>
<blockquote>
<p>这个时候，Gradle 会自动生成 SettingFragmentArgs 以及 RootFragmentDirections 这两个类，在 generatedJava 这个文件夹下的包内。如果没有自动生成的话，clean 一下或是 rebuild 项目都行。</p>
</blockquote>
<p>现在就能直接通过 setNickName 的形式来设置待传递的值了。</p>
<pre><code class="language-java">String nickName = &quot;master&quot;;
RootFragmentDirections.ActionRootFragmentToSettingsFragment action =
        RootFragmentDirections.actionRootFragmentToSettingsFragment().setNickName(nickName);

Navigation.findNavController(btnToSettings)
        .navigate(action);
</code></pre>
<p>在 SettingsFragment 我们需要把值取出来，然后显示在屏幕上。</p>
<pre><code class="language-java">String nickName = SettingsFragmentArgs.fromBundle(getArguments()).getNickName();
tvNickName.setText(nickName);
</code></pre>
<p>怎么样，是不是很简单，这比之前我们用 Bundle 传值要方便的多啦，而且再也不用担心 Key 写错的问题了。真香。</p>
<p>好了，Navigation 的基本学习就到这了，感觉真的挺不错的。可以考虑用用了，不过现在好像主页面都是四个或是五个 Tab 页面，这用 Navigation 怎么实现呀？Google 早就替我们想好了。</p>
<h2 id="bottomnavigationview">BottomNavigationView</h2>
<p>来，我们新建一个 Activity，然后打开布局文件的图形化工具页面，用之前我们添加 NavHostFragment 组件的方式来添加一个 BottomNavigationView，然后让这个组件位于整个页面的底部。页面的其余部分全部都留给 NavHostFragment。因为我们又引入了一个新的 Fragment 管理栈，所以，需要再次新建一个 Navigation Graph 文件 tab_navigation。</p>
<p>下面就是 activity_tab.xml 以及 tab_navigation.xml 的代码。</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.TabActivity&quot;&gt;

    &lt;com.google.android.material.bottomnavigation.BottomNavigationView
        android:id=&quot;@+id/nv_bottom_menu&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;48dp&quot;
        app:itemHorizontalTranslationEnabled=&quot;false&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot; /&gt;

    &lt;fragment
        android:id=&quot;@+id/fragment3&quot;
        android:name=&quot;androidx.navigation.fragment.NavHostFragment&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;0dp&quot;
        app:defaultNavHost=&quot;true&quot;
        app:layout_constraintBottom_toTopOf=&quot;@+id/nv_bottom_menu&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:navGraph=&quot;@navigation/tab_navigation&quot; /&gt;
&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;

&lt;navigation xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:id=&quot;@+id/tab_navigation&quot;&gt;

&lt;/navigation&gt;
</code></pre>
<p>接下来干什么呢？刚才我们创建是容器，用于容纳 Fragment 的，现在来创建三个 Fragment，这三个 Fragment 是用于填充进容器的内容。</p>
<p>分别是 FeedFragment、TimerFragment、MineFragment。这三个 Fragment 我们还是分别显示自己的名称。布局文件里也就一个 TextView，Java 代码中什么也不做，仅仅是用来显示。</p>
<p>有了三个 Fragment，我们现在去 tab_navigation 把这三个 Fragment 都添加进去。</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;navigation xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:id=&quot;@+id/tab_navigation&quot;
    app:startDestination=&quot;@id/feedFragment&quot;&gt;

    &lt;fragment
        android:id=&quot;@+id/feedFragment&quot;
        android:name=&quot;me.monster.blogtest.tab.FeedFragment&quot;
        android:label=&quot;fragment_feed&quot;
        tools:layout=&quot;@layout/fragment_feed&quot; /&gt;
    &lt;fragment
        android:id=&quot;@+id/timerFragment&quot;
        android:name=&quot;me.monster.blogtest.tab.TimerFragment&quot;
        android:label=&quot;fragment_timer&quot;
        tools:layout=&quot;@layout/fragment_timer&quot; /&gt;
    &lt;fragment
        android:id=&quot;@+id/mineFragment&quot;
        android:name=&quot;me.monster.blogtest.tab.MineFragment&quot;
        android:label=&quot;fragment_mine&quot;
        tools:layout=&quot;@layout/fragment_mine&quot; /&gt;
&lt;/navigation&gt;
</code></pre>
<p>现在，我们容器有了，内容有了，只差一个媒介，把它们进行关联了。打开 activity_tab 的图形化界面，在左侧有一些属性，其中有一个属性是 menu。menu？就是那个经常用于页面右上角的 menu？它怎么会出现在这边？点击 menu 行最右边的按钮。</p>
<figure data-type="image" tabindex="10"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190628235937692.png" alt="image-20190628235937692" loading="lazy"></figure>
<p>弹出一个对话框，好像和一开始创建 NavHostFragment 是一样的，不同的是，当时有待选择的 Navigation Graph 文件，现在我们没有 menu 文件，那就创建一个吧。</p>
<figure data-type="image" tabindex="11"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190629000519557.png" alt="image-20190629000519557" loading="lazy"></figure>
<p>现在我们也有了 menu 文件。</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;menu xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

    &lt;item
        android:id=&quot;@+id/feedFragment&quot;
        android:icon=&quot;@drawable/ic_tab_feed&quot;
        android:title=&quot;Feed&quot; /&gt;
    &lt;item
        android:id=&quot;@+id/timerFragment&quot;
        android:icon=&quot;@drawable/ic_tab_timer&quot;
        android:title=&quot;Timer&quot; /&gt;
    &lt;item
        android:id=&quot;@+id/mineFragment&quot;
        android:icon=&quot;@drawable/ic_tab_mine&quot;
        android:title=&quot;Mine&quot; /&gt;
&lt;/menu&gt;
</code></pre>
<p>现在再回去看 tab_activity.xml 发现 preview 已经变成了这样的。Cool</p>
<figure data-type="image" tabindex="12"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190629110347302.png" alt="image-20190629110347302" loading="lazy"></figure>
<p>难道 Menu 就是那个把内容 (Fragment) 与容器 (NavHostFragment) 进行建立关系的媒介？是也不是，有那么一点关系，不过不太准确。还记得之前我们用与 RootFragment 和 SettingsFragment 进行切换页面的方式吗？一个是前进到下一个页面，一个是返回上一个页面，虽然最终的行为不同，但是它们都使用到了一个叫 <strong>NavController</strong> 的类，这个类实际上就是实现在 Fragment 之间进行跳转的类。</p>
<pre><code class="language-java">Navigation.findNavController(btnToSettings).navigate(action);
Navigation.findNavController(btnToRoot).popBackStack();
</code></pre>
<p>那我们是不是可以通过 Navigation Controller 并结合底部导航菜单的点击事件来对 Fragment 进行控制，从而实现 Fragment 之间的切换？是这样的，没错，不过 Google 帮助我们完成了很多复杂的事情，我们只需要在 TabActivity 中添加下面这些代码即可。</p>
<pre><code class="language-java">private void setUpNavBottom() {
    NavHostFragment hostFragment = (NavHostFragment) getSupportFragmentManager()
        .findFragmentById(R.id.fragment3);
    BottomNavigationView navMenu = findViewById(R.id.nv_bottom_menu);
    if (hostFragment != null) {
        NavController navController = hostFragment.getNavController();
        NavigationUI.setupWithNavController(navMenu, navController);
    }
}
</code></pre>
<p>第一行，findFragmentById 里填写的 id 就是我们在 tab_activity.xml 中 name 属性是 NavHostFragment 节点的 id。</p>
<p>然后再通过 <code>NavigationUI.setupWithNavController()</code> 将二者进行想管理，这样只要我们点击底部导航菜单就是自动实现 Fragment 的之间的切换，完全不需要开发者自己去写那么控制逻辑。事实上，<code>NavigationUI.setupWithNavController()</code> 这个方法有很多重载方法，不仅仅只是用在 BottomNavigationView，还有 NavigationView 等，在这里就不一一介绍了感兴趣的可以去试试。现在来看看效果。</p>
<figure data-type="image" tabindex="13"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/Blognav_bottomNav-1779718.gif" alt="Blognav_bottomNav" loading="lazy"></figure>
<h2 id="deeplink">DeepLink</h2>
<p>来来来，回顾一下刚才我们介绍的 Navigation Graph，它就是用于描述 Fragment 或者说用于描述内容信息的，刚才我们尝试了子节点 Fragment 的 action（页面跳转）与 arguments（Bundle 传值）节点，其实他还有一个子节点 deepLink。</p>
<p>不知道，你有没有遇到那种情况，朋友在微信上分享你一个连接，你一点开，页面上提示你使用微信的在浏览器打开，你在一点开发现，发现跳转到了一个应用的页面上去了。这种跳转方式在 Navigation 这个导航框架内叫做 deepLink。让我们来实现一下吧。</p>
<p>我们需要准备一个 Fragment，就叫 DeepLinkFragment 好了，这个页面我们跟之前的 Fragment 一样只显示 DeepLinkFragment 这个文字好了。layout 布局文件及 Java 代码就不贴了。现在再来看 Navigation Graph 中怎么写。</p>
<pre><code class="language-xml">fragment
    android:id=&quot;@+id/deepLinkFragment&quot;
    android:name=&quot;me.monster.blogtest.fragment.DeepLinkFragment&quot;
    android:label=&quot;fragment_deep&quot;
    tools:layout=&quot;@layout/fragment_deep&quot; &gt;
    &lt;deepLink
        android:id=&quot;@+id/deepLink&quot;
        app:uri=&quot;www.example.com/{myarg}&quot; /&gt;
&lt;/fragment&gt;
</code></pre>
<p>是的，你没有看错，在 Navigation Graph 中就多了这么点东西，然后记得一定要记得在 manifest 的承载 DeepLinkFragment 的 Activity 节点内引入你的 Navigation Graph。</p>
<blockquote>
<ol>
<li>那里填的 url 后面大括号包裹着的是传入 DeepLinkFragment 的值，<code>myarg</code> 是 key，通过 Bundle 进行传递；</li>
<li>我在写这篇博客的时候，有两个 Navigation Graph 文件，一个是用于 RootFragment 与 SettingsFragment 进行跳转的 navigation.xml，一个是用于底部导航菜单栏的 tab_navigation.xml，我把 DeepLinkFragment 放在了 navigation.xml 中，所以下面的值是 @navigation/navigation。</li>
</ol>
</blockquote>
<pre><code class="language-xml">&lt;nav-graph android:value=&quot;@navigation/navigation&quot; /&gt;
</code></pre>
<p>来试一下，看看效果吧。</p>
<figure data-type="image" tabindex="14"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/Blognav_deepLink.gif" alt="Blognav_deepLink" loading="lazy"></figure>
<p>好了，我们整个 Navigation 的学习到这里也告一段落了，结束之前让我们用一幅图来回顾一下 Navigation。</p>
<figure data-type="image" tabindex="15"><img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/image-20190629150736867.png" alt="image-20190629150736867" loading="lazy"></figure>
<p>本文首发于<a href="https://jiangjiwei.site/">个人博客</a>，文中全部源代码已上传至 <a href="https://github.com/CherryLover/BlogTest">GitHub</a>，喜欢的麻烦点个🌟。<br>
本文封面图：Photo by <a href="https://unsplash.com/@jbcreate_?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Joseph Barrientos</a> on <a href="https://unsplash.com/search/photos/navigation?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>

            </div>
          </article>
        </div>
    
        
          <div class="next-post">
            <div class="next">下一篇</div>
            <a href="https://cherrylover.github.io/post/java-fan-xing/">
              <h3 class="post-title">
                Java 泛型
              </h3>
            </a>
          </div>  
        

        
          
            <link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css">
<script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script>

<div id="gitalk-container"></div>

<script>

  var gitalk = new Gitalk({
    clientID: 'f5cc70d7d50fba65cf69',
    clientSecret: '1d018e38ce9cdd399682bdd76690decfef00a051',
    repo: 'CherryLover.github.io',
    owner: 'CherryLover',
    admin: ['CherryLover'],
    id: location.pathname,      // Ensure uniqueness and length less than 50
    distractionFreeMode: false  // Facebook-like distraction free mode
  })

  gitalk.render('gitalk-container')

</script>

          

          
        
    
        <div class="site-footer">
  Powered by <a href="https://github.com/getgridea/gridea" target="_blank">Gridea</a>
</div>

<script>
  hljs.initHighlightingOnLoad()
</script>

      </div>
    </div>
  </body>
</html>
